package com.googlecode.mastercrud.persistence.hibernate;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.Entity;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;

import com.googlecode.mastercrud.exceptions.DAOException;
import com.googlecode.mastercrud.persistence.Criterias;
import com.googlecode.mastercrud.persistence.GenericDAO;
import com.googlecode.mastercrud.persistence.OrderColumn;
import com.googlecode.mastercrud.persistence.Result;

@Named
public class DAOHibernate<E> implements GenericDAO<E> {
	protected Class<E> clazz;
	
	@Inject
	private SessionFactory sessionFactory;
	
	public DAOHibernate() {
		this.getEntityClass();
	}

	@SuppressWarnings("unchecked")
	public void getEntityClass() {
		try {
			if (clazz == null) {
				final Type thisType = getClass().getGenericSuperclass();
				final Type type;
				if (thisType instanceof ParameterizedType) {
					type = ((ParameterizedType) thisType).getActualTypeArguments()[0];
				} else if (thisType instanceof Class) {
					type = ((ParameterizedType) ((Class<E>) thisType).getGenericSuperclass()).getActualTypeArguments()[0];
				} else {
					throw new IllegalArgumentException("Problem handling type construction for " + getClass());
				}
				if (type instanceof Class) {
					this.clazz = (Class<E>) type;
				} else if (type instanceof ParameterizedType) {
					this.clazz = (Class<E>) ((ParameterizedType) type).getRawType();
				}
			}
		} catch (Exception e) { }
	}

	@Override
	public void delete(E entity) throws DAOException {
		Session session = getSessionFactory().getCurrentSession();
		Transaction tx = session.beginTransaction();
		session.delete(entity);
		session.flush();
		tx.commit();
	}

	@Override
	public void insert(E entity) throws DAOException {
		Session session = getSessionFactory().getCurrentSession();
		try {
			Transaction tx = session.beginTransaction();
			session.save(entity);
			tx.commit();
		} catch (Exception ex) {
			throw new DAOException(ex);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public Result<E> list(Criterias criterias) throws DAOException {
		criterias.getDefaultOrderFromFields(clazz);
		Result<E> resultado = new Result<E>();
		Session session = getSessionFactory().getCurrentSession();
		Transaction tx = session.beginTransaction();
		Criteria criteria = session.createCriteria(clazz);
		criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
		if (criterias.getMaxResults() > 0)
			criteria.setMaxResults(criterias.getMaxResults());
		if (criterias.getFirstResult() > 0) {
			criteria.setFirstResult(criterias.getFirstResult());
		} else {
			criteria.setFirstResult(0);
		}
		for(OrderColumn oc:criterias.getOrders()) {
			if ( oc.isAsc() ) {
				criteria.addOrder(Order.asc(oc.getName()));
			} else {
				criteria.addOrder(Order.desc(oc.getName()));
			}
		}
		Collection<Entity> c = criteria.list();
		resultado.setResult((List<E>) c);

		criteria = session.createCriteria(clazz);
		criteria.setProjection(Projections.rowCount());
		if (criteria.uniqueResult() != null) {
			resultado.setTotal((Integer) criteria.uniqueResult());
		} else {
			resultado.setTotal(0);
		}
		tx.commit();
		return resultado;
	}

	@SuppressWarnings("unchecked")
	@Override
	public Result<E> listByCriteria(Criterias criterias) throws DAOException {
		criterias.getDefaultOrderFromFields(clazz);
		Session session = getSessionFactory().getCurrentSession();
		Transaction tx = session.beginTransaction();
		Query query = session.getNamedQuery(criterias.getQuery());
		query.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
		if (criterias.getMaxResults() > 0) {
			query.setMaxResults(criterias.getMaxResults());
		}
		if (criterias.getFirstResult() > 0) {
			query.setFirstResult(criterias.getFirstResult());
		}
		for (String key : criterias.getParameters().keySet()) {
			Object param = criterias.getParameters().get(key);
			if (Collection.class.isInstance(param)) {
				query.setParameterList(key, (Collection<E>) param);
			} else {
				query.setParameter(key, param);
			}
		}
		Result<E> resultado = new Result<E>();
		resultado.setResult(query.list());

		Criteria criteria = session.createCriteria(clazz);
		criteria.setProjection(Projections.rowCount());
		if (criteria.uniqueResult() != null) {
			resultado.setTotal((Integer) criteria.uniqueResult());
		} else {
			resultado.setTotal(0);
		}
		tx.commit();
		return resultado;
	}

	@SuppressWarnings("unchecked")
	@Override
	public Result<E> listByExample(E entity, Criterias criterias)
			throws DAOException {
		criterias.getDefaultOrderFromFields(clazz);
		Session session = getSessionFactory().getCurrentSession();
		Transaction tx = session.beginTransaction();
		Criteria criteria = session.createCriteria(entity.getClass());
		Example example = Example.create(entity).enableLike(MatchMode.ANYWHERE)
				.ignoreCase();
		criteria.add(example);
		this.gerarExemplosAninhados(criteria, entity);
		for(OrderColumn oc:criterias.getOrders()) {
			if ( oc.isAsc() ) {
				criteria.addOrder(Order.asc(oc.getName()));
			} else {
				criteria.addOrder(Order.desc(oc.getName()));
			}
		}
		List<E> c = criteria.list();
		Result<E> resultado = new Result<E>();
		resultado.setResult(c);

		criteria = session.createCriteria(entity.getClass());
		criteria.add(example);
		criteria.setProjection(Projections.rowCount());
		if (criteria.uniqueResult() != null) {
			resultado.setTotal((Integer) criteria.uniqueResult());
		} else {
			resultado.setTotal(0);
		}
		tx.commit();
		return resultado;
	}

	@SuppressWarnings("unchecked")
	@Override
	public E load(Serializable id) throws DAOException {
		Session session = getSessionFactory().getCurrentSession();
		Transaction tx = session.beginTransaction();
		E entidade = (E) session.load(clazz, id);
		tx.commit();
		return entidade;
	}

	@Override
	public void save(E entity) throws DAOException {
		Field field;
		try {
			field = entity.getClass().getField("id");
		} catch (SecurityException e) {
			throw new DAOException(e);
		} catch (NoSuchFieldException e) {
			throw new DAOException(e);
		}
		boolean accessible = field.isAccessible();
		field.setAccessible(true);
		Object value = null;
		try {
			value = field.get(entity);
		} catch (IllegalArgumentException e) {
			throw new DAOException(e);
		} catch (IllegalAccessException e) {
			throw new DAOException(e);
		}
		field.setAccessible(accessible);
		if (value == null) {
			this.insert(entity);
		} else {
			this.update(entity);
		}
	}

	@Override
	public void update(E entity) throws DAOException {
		Session session = getSessionFactory().getCurrentSession();
		Transaction tx = session.beginTransaction();
		try {
			session.update(entity);
		} catch (Exception e) {
			session.merge(entity);
		}
		session.flush();
		tx.commit();
	}

	@SuppressWarnings("all")
	private void gerarExemplosAninhados(Criteria criteria, E entidade) throws DAOException {
		Method[] methods = entidade.getClass().getMethods();
		for (Method method : methods) {
			if (method.getName().indexOf("get") > -1) {
				try {
					Object o = method.invoke(entidade, null);
					Annotation[] annotations = o.getClass().getDeclaredAnnotations();
					boolean instanceOf = false;
					for(Annotation annotation : annotations) {
						if ( annotation instanceof Entity ) {
							instanceOf = true;
							break;
						}
					}
					if (instanceOf) {
						Example example = Example.create(o).enableLike(MatchMode.ANYWHERE).ignoreCase();
						String m = method.getName().substring(3);
						m = m.substring(0, 1).toLowerCase() + m.substring(1);
						criteria.createCriteria(m).add(example);
					}
				} catch (IllegalArgumentException e) {
					throw new DAOException(e);
				} catch (IllegalAccessException e) {
					throw new DAOException(e);
				} catch (InvocationTargetException e) {
					throw new DAOException(e);
				}
			}
		}
	}

	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}
}
